跳到主要内容

Go 网络包 httputil-反向代理

反向代理是什么?

正向代理:隐藏真正的客户端

image.png

反向代理:隐藏后端服务器(Nginx)

imaged457bd8a179b01a0.png

简单使用例

编写一个被代理的服务

package main

import (
"log"
"net/http"
)

// 这里创建一个类型是为了实现 Handler 接口
type server int

func (h *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
w.Write([]byte("Hello World!\n"))
}

func main() {
var s server
http.ListenAndServe("localhost:7070", &s)
}

启动这个服务后

$ curl http://127.0.0.1:7070
Hello World!

再编写一个反向代理服务

package main

import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)

// NewProxy 拿到 targetHost 后,创建一个反向代理
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
// 返回一个单主机代理对象
return httputil.NewSingleHostReverseProxy(url), nil
}

// ProxyRequestHandler 使用 proxy 处理请求
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
// 返回一个代理方法
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}

func main() {
// 初始化反向代理并传入真正后端服务的地址(被代理的服务器)
proxy, err := NewProxy("http://127.0.0.1:7070")
if err != nil {
panic(err)
}

// 使用 proxy 处理所有请求到你的服务
http.HandleFunc("/", ProxyRequestHandler(proxy))
log.Fatal(http.ListenAndServe(":8080", nil))
}

这时访问 :8080 就能代理到 :7070 那里去了

$ curl http://127.0.0.1:8080
Hello World!

关键的代码就是 NewSingleHostReverseProxy 这个方法,该方法返回了一个 ReverseProxy 对象,在 ReverseProxy 中的 ServeHTTP 方法实现了这个具体的过程,主要是对源 http 包头进行重新封装,而后发送到后端服务器。

代理时修改响应

修改响应,例如添加一个请求头字段

func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}

proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ModifyResponse = modifyResponse()
return proxy, nil
}

func modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
resp.Header.Set("X-Proxy", "Magical")
return nil
}
}

这个 ReverseProxy 提供了一个用来修改响应的执行

在 modifyResponse 中,可以返回一个错误(如果你在处理响应发生了错误), 如果你设置了 proxy.ErrorHandler, modifyResponse 返回错误时会自动调用 ErrorHandler 进行错误处理。

func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}

proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ModifyResponse = modifyResponse()
proxy.ErrorHandler = errorHandler()
return proxy, nil
}

// 抛出一个异常
func modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
return errors.New("response body is invalid")
}
}

// 捕获异常,这里可以进行错误处理
func errorHandler() func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, req *http.Request, err error) {
fmt.Printf("Got error while modifying response: %v \n", err)
// 重新把请求响应回去
w.Write([]byte(err.Error() + "\n"))
}
}

响应结果:

$ curl http://127.0.0.1:8080
response body is invalid

代理时修改请求

func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}

proxy := httputil.NewSingleHostReverseProxy(url)

originalDirector := proxy.Director // 原本执行的流程

proxy.Director = func(req *http.Request) {
originalDirector(req) // 需要执行原本的代理流程
modifyRequest(req)
}

proxy.ModifyResponse = modifyResponse()
proxy.ErrorHandler = errorHandler()
return proxy, nil
}

func modifyRequest(req *http.Request) {
req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}

不代理自己

如下,子进程可以通过设置环境变量的方式去代理

parentEnv := os.Environ()
// 设置代理环境变量
parentEnv = append(parentEnv, fmt.Sprintf("HTTP_PROXY=http://127.0.0.1:%d", t.env.GetCfg().Mock.Port))
parentEnv = append(parentEnv, fmt.Sprintf("http_proxy=http://127.0.0.1:%d", t.env.GetCfg().Mock.Port))
parentEnv = append(parentEnv, fmt.Sprintf("HTTPS_PROXY=http://127.0.0.1:%d", t.env.GetCfg().Mock.Port))
parentEnv = append(parentEnv, fmt.Sprintf("https_proxy=http://127.0.0.1:%d", t.env.GetCfg().Mock.Port))
command.Env = parentEnv

但是当这个全局代理和反向代理混合使用时,就会出现回环的情况,所以需要避免它代理自己,解决这个问题之前先来问一下,设置了代理,Golang 的 http 库是在哪里读取环境变量的呢?

它是在 http 包的 Transport 里面定义的 Proxy 函数里调用的,可以看到默认的这个 Transport 变量,里面使用 ProxyFromEnvironment 函数去读取环境变量

// net/http/transport.go
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

// ...

func ProxyFromEnvironment(req *Request) (*url.URL, error) {
return envProxyFunc()(req.URL)
}

// ...
func envProxyFunc() func(*url.URL) (*url.URL, error) {
envProxyOnce.Do(func() {
envProxyFuncValue = httpproxy.FromEnvironment().ProxyFunc()
})
return envProxyFuncValue
}

所以只需自己创建 Transport 不去实现这个 Proxy 函数就行了,如下

// ...
func ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
return
}
p := httputil.ReverseProxy{}
// 防止使用代理再次回到本方法中
p.Transport = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
p.Director = func(request *http.Request) {}
p.ServeHTTP(rw, r)
}

References

go tls实现TLS 服务器和客户端通讯 HTTP(S) Proxy in Golang in less than 100 lines of code 理解HTTP CONNECT通道